﻿using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace InfoPathHelpers.Attachments
{
    /// <summary>
    /// AttachmentEncoder encodes file data into the format expected by InfoPath for use in file attachment nodes.
    /// </summary>
    public static class AttachmentEncoder
    {
        /// <summary>
        /// Returns a Base64 encoded string.
        /// </summary>
        /// <returns>String</returns>
        public static string EncodeAttachment(FileInfo file)
        {
            // This memory stream will hold the InfoPath file attachment buffer before Base64 encoding.
            MemoryStream Stream = new MemoryStream();

            // Obtain the file information.
            using(var FileStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (BinaryReader Reader = new BinaryReader(FileStream))
            {
                string FileName = file.Name;

                uint FileNameLength = (uint)FileName.Length + 1;

                byte[] FileNameBytes = Encoding.Unicode.GetBytes(FileName);

                using (BinaryWriter Writer = new BinaryWriter(Stream))
                {
                    // Write the InfoPath attachment signature. 
                    Writer.Write(new byte[] { 0xC7, 0x49, 0x46, 0x41 });

                    // Write the default header information.
                    Writer.Write((uint)0x14);	// size
                    Writer.Write((uint)0x01);	// version
                    Writer.Write((uint)0x00);	// reserved

                    // Write the file size.
                    Writer.Write((uint)Reader.BaseStream.Length);

                    // Write the size of the file name.
                    Writer.Write((uint)FileNameLength);

                    // Write the file name (Unicode encoded).
                    Writer.Write(FileNameBytes);

                    // Write the file name terminator. This is two nulls in Unicode.
                    Writer.Write(new byte[] { 0, 0 });

                    // Iterate through the file reading data and writing it to the outbuffer.
                    byte[] Buffer = new byte[64 * 1024];
                    int BytesRead = 0;

                    while ((BytesRead = Reader.Read(Buffer, 0, Buffer.Length)) > 0)
                    {
                        Writer.Write(Buffer, 0, BytesRead);
                    }
                }
            }


            // This MemoryStream will hold the Base64 encoded InfoPath attachment.
            MemoryStream OutStream = new MemoryStream();

            using (BinaryReader Reader = new BinaryReader(new MemoryStream(Stream.ToArray())))
            {
                // Create a Base64 transform to do the encoding.
                ToBase64Transform Base64Transform = new ToBase64Transform();

                byte[] Buffer = new byte[Base64Transform.InputBlockSize];
                byte[] Output = new byte[Base64Transform.OutputBlockSize];

                int BytesRead = 0;

                while ((BytesRead = Reader.Read(Buffer, 0, Buffer.Length)) > 0)
                {
                    if (BytesRead == Buffer.Length)
                        Base64Transform.TransformBlock(Buffer, 0, BytesRead, Output, 0);
                    else
                        Output = Base64Transform.TransformFinalBlock(Buffer, 0, BytesRead);

                    OutStream.Write(Output, 0, Output.Length);
                }
            }

            OutStream.Close();

            return Encoding.ASCII.GetString(OutStream.ToArray());
        }

        public static AttachmentInfo GetAttachmentInfo(string attachmentData)
        {
            var AttachmentInfo = new AttachmentInfo();

            using (MemoryStream InputStream = new MemoryStream(Convert.FromBase64String(attachmentData)))
            using (BinaryReader Reader = new BinaryReader(InputStream))
            {
                //Position the reader to obtain the file size.
                var HeaderData = Reader.ReadBytes(16);
                AttachmentInfo.FileSize = (int)Reader.ReadUInt32();
                var AttachmentNameLength = (int)Reader.ReadUInt32() * 2;
                var FileNameBytes = Reader.ReadBytes(AttachmentNameLength);
                //InfoPath uses UTF8 encoding.
                AttachmentInfo.FileName =  Encoding.Unicode.GetString(FileNameBytes, 0, AttachmentNameLength - 2);
            }

            return AttachmentInfo;
        }

        /// <summary>
        /// Decodes the attachment and saves it to a temporary file.
        /// </summary>
        /// <param name="attachmentData">The Base64 encoded attachment.</param>
        /// <returns>A FileInfo pointing to the temporary file.</returns>
        public static FileInfo DecodeAttachment(string attachmentData)
        {
            return DecodeAttachment(attachmentData, null, true);
        }

        /// <summary>
        /// Decodes the attachment and saves it to a temporary file.
        /// </summary>
        /// <param name="attachmentData">The Base64 encoded attachment.</param>
        /// <param name="fileName">The path to save the attachment to.</param>
        /// <param name="overwrite">True to overwite existing files with the same name; otherwise false.</param>
        /// <returns>A FileInfo pointing to the decoded file. Or the existing file if not overwritten.</returns>
        public static FileInfo DecodeAttachment(string attachmentData, string fileName, bool overwrite)
        {
            using (MemoryStream InputStream = new MemoryStream(Convert.FromBase64String(attachmentData)))
            using (BinaryReader Reader = new BinaryReader(InputStream))
            {
                //Position the reader to obtain the file size.
                var HeaderData = Reader.ReadBytes(16);
                var FileSize = (int)Reader.ReadUInt32();
                var AttachmentNameLength = (int)Reader.ReadUInt32() * 2;
                var FileNameBytes = Reader.ReadBytes(AttachmentNameLength);
                //InfoPath uses UTF8 encoding.
                var AttachmentName = Encoding.Unicode.GetString(FileNameBytes, 0, AttachmentNameLength - 2);
                var AttachmentExtension = Path.GetExtension(AttachmentName);
                var AttachmentData = Reader.ReadBytes(FileSize);
                FileInfo File;
                if (string.IsNullOrEmpty(fileName))
                {
                    File = new FileInfo(Path.GetTempFileName().Replace(".tmp", AttachmentExtension));
                }
                else
                {
                    File = new FileInfo(fileName);
                }

                if (!File.Exists || overwrite)
                {
                    using (var OutputStream = File.Open(FileMode.Create, FileAccess.Write, FileShare.None))
                    using (var Writer = new BinaryWriter(OutputStream))
                    {
                        Writer.Write(AttachmentData);
                    }
                }

                return File;
            }
        }
    }
}
